/*
 * Copyright (c) 2008 Ricardo Rocha, rocha.porto@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package org.gliding.kumulus.client;

import java.util.ArrayList;
import java.util.Date;

import com.google.gwt.i18n.client.DateTimeFormat;
import com.google.gwt.user.client.ui.ClickListener;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.HTML;
import com.google.gwt.user.client.ui.HorizontalPanel;
import com.google.gwt.user.client.ui.Hyperlink;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;

/**
 * Class defining the Calendar component.
 * 
 * @author Ricardo Rocha <rocha.porto@gmail.com>
 * @version $Rev:: $
 * 
 * @see org.gliding.kumulus.client.Planning
 */
public class Calendar extends Composite implements ClickListener {

    /**
     * The list of dates selected in the calendar.
     */
    private Date[] datesSelected;
    
    /**
     * The date considered to build the panel of the calendar;
     */
    private Date datePanel;
    
    /**
     * The panel containing the month selector panel, the weekday header panel
     * and the month days list panel.
     */
    private VerticalPanel calendarPanel;
    
    /**
     * The link to move the panel to the previous month.
     */
    private Hyperlink prevMonthLink;
    
    /**
     * The link to select all days in the current month;
     */
    private Hyperlink monthLink;
    
    /**
     * The link to move the panel to the next month.
     */
    private Hyperlink nextMonthLink;
    
    /**
     * The panel containing the list of days belonging to the month corresponding
     * to the current panel date.
     */
    private VerticalPanel monthDayListPanel = new VerticalPanel();
    
    /**
     * Handles formatting of Date object into a month string.
     */
    public static DateTimeFormat fullMonthYearFormat = DateTimeFormat.getFormat("MMMM yyyy");
    
    /**
     * Handles formatting of Date object into a short month string.
     */
    public static DateTimeFormat dayShortMonthYeartFormat = DateTimeFormat.getFormat("dd MMM yyyy");
    
    /**
     * Period size enumeration components.
     */
    public static int DAILY = 0;
    public static int WEEKLY = 1;
    public static int MONTHLY = 2;
    public static int NEXT4DAYS = 3;
    
    /**
     * The currently selected period size mode for the calendar 
     * (one of DAILY, WEEKLY, MONTHLY, NEXT4DAYS as defined above). 
     */
    private int selectionMode;
    
    /**
     * The different subscribers to updates of the date on this widget.
     */
    private ArrayList updateListeners = new ArrayList();
    
	/**
	 * Class constructor.
	 * 
	 * Initializes the Calendar widget, adding all the required selector bits,
	 * setting an initial selected date and building the panel.	 * 
     * 
     * @param dates: The list of dates to preselect
	 */
	public Calendar(Date[] dates) {
		
		// The overall container for all the components belonging to the widget
        SimplePanel calendarPanel = new SimplePanel();
    
        // Contains the month selector, the weekdays header and the month day list panels
        this.calendarPanel = new VerticalPanel(); 
        this.calendarPanel.setStyleName("kumulus-Calendar");
        
        // The top month selector panel
        HorizontalPanel monthSelectorPanel = new HorizontalPanel();
        monthSelectorPanel.setStyleName("kumulus-Calendar-msp");
        monthSelectorPanel.setWidth("100%");
        prevMonthLink = new Hyperlink("«", "previousMonth");
        prevMonthLink.addClickListener(this);
        monthSelectorPanel.add(prevMonthLink);
        monthSelectorPanel.setCellWidth(prevMonthLink, "15%");
        monthSelectorPanel.setCellHorizontalAlignment(prevMonthLink, 
                HorizontalPanel.ALIGN_CENTER);
        monthLink = new Hyperlink();
        monthLink.addClickListener(this);
        monthSelectorPanel.add(monthLink);
        monthSelectorPanel.setCellWidth(monthLink, "70%");
        monthSelectorPanel.setCellHorizontalAlignment(monthLink, 
                HorizontalPanel.ALIGN_CENTER);
        nextMonthLink = new Hyperlink("»", "nextMonth");
        nextMonthLink.addClickListener(this);
        monthSelectorPanel.add(nextMonthLink);
        monthSelectorPanel.setCellHorizontalAlignment(nextMonthLink, 
                HorizontalPanel.ALIGN_CENTER);
        this.calendarPanel.add(monthSelectorPanel);
        
        // The week days header panel
        HorizontalPanel weekDayHeaderPanel = new HorizontalPanel();
        weekDayHeaderPanel.setWidth("100%");
        weekDayHeaderPanel.setHorizontalAlignment(HorizontalPanel.ALIGN_CENTER);
        weekDayHeaderPanel.setStyleName("kumulus-Calendar-wdhp");
        weekDayHeaderPanel.add(new HTML("M"));
        weekDayHeaderPanel.add(new HTML("T"));
        weekDayHeaderPanel.add(new HTML("W"));
        weekDayHeaderPanel.add(new HTML("T"));
        weekDayHeaderPanel.add(new HTML("F"));
        weekDayHeaderPanel.add(new HTML("S"));
        weekDayHeaderPanel.add(new HTML("S"));
        this.calendarPanel.add(weekDayHeaderPanel);
        
        this.calendarPanel.add(this.monthDayListPanel);

        // Add the internal panel to the main calendar panel
        calendarPanel.add(this.calendarPanel);
        
        // Set the default preselected dates, or the given argument if there was one
        Date[] selectedDates = { new Date() };
        if (dates != null) {
            selectedDates = dates;
        }
        // The panel date is the one used to build the calendar panel
        this.setPanelDate(selectedDates[0]);
        // And these are the actually selected dates (not necessarily the same)
        this.setDatesFromDate(selectedDates[0]);
        // We set WEEKLY as the default period size selected
        this.setSelectionMode(Calendar.WEEKLY);
        
        // Initialize this widget and set the CSS style
        initWidget(calendarPanel);
	}
    
    /**
     * Class constructor, setting the preselected dates to the current date only.
     */
    public Calendar() {
        this(null);
    }
    
    /**
     * Moves the Calendar panel to the previous month.
     * 
     * It does not change the currently selected dates, but it does change the panel date,
     * which is the one used as a reference to build the calendar panel.
     */
    public void movePanelToPrevious() {
        if (this.datePanel.getMonth() == 0) {
            this.datePanel.setYear(this.datePanel.getYear()-1);
            this.datePanel.setMonth(11);
        } else {
            this.datePanel.setMonth(this.datePanel.getMonth()-1);
        }
        this.refreshPanel();
    }
    
    /**
     * Moves the Calendar panel to the next month.
     * 
     * It does not change the currently selected dates, but it does change the panel date,
     * which is the one used as a reference to build the calendar panel.
     */
    public void movePanelToNext() {
        if (this.datePanel.getMonth() == 11) {
            this.datePanel.setYear(this.datePanel.getYear()+1);
            this.datePanel.setMonth(0);
        } else {
            this.datePanel.setMonth(this.datePanel.getMonth()+1);
        }
        this.refreshPanel();
    }
    
    /**
     * Sets the date to be considered when building the Calendar panel.
     * 
     * @param panelDate The Date object to use to build the Calendar panel
     */
    public void setPanelDate(Date panelDate) {
        this.datePanel = new Date(panelDate.getYear(), panelDate.getMonth(), 1);
    }
    
    /**
     * Adds the given dates to the list of currently selected dates.
     * 
     * This is useful when selecting the dates in a more dynamic way, like holding the mouse
     * down and dragging.
     * 
     * @param dates The Date objects to be added to the current list of selected dates
     */
    public void addDatesSelected(Date[] dates) {
        
    }
    
    /**
     * Sets the currently selected dates starting from the one passed as an argument.
     * 
     * The actual list depends on the currently selected period size. For example, if the
     * current period size of this Calendar is NEXT4DAYS, then the selected dates will be
     * the given one and the 3 days following it. The same works for WEEKLY (given day + 6 following days)
     * and monthly (given day + x following).
     * 
     * @param date The Date object to be used as a reference to set the set of selected dates
     */
    public void setDatesFromDate(Date date) {
        ArrayList dates = new ArrayList();
        
        if (this.getSelectionMode() == Calendar.DAILY) {
            dates.add(date);
        } else if (this.getSelectionMode() == Calendar.NEXT4DAYS) {
            long startDateMillis = date.getTime();
            for (int i=0; i<=3; i++) {
                dates.add(new Date(startDateMillis + ((long)86400000 * i)));
            }
        } else if (this.getSelectionMode() == Calendar.WEEKLY) {
            long startDateMillis = date.getTime() - ((date.getDay()-1) * 86400000);
            if (date.getDay() == 0) {
                startDateMillis = date.getTime() - (6 * 86400000);
            }
            for (int i=0; i<=6; i++) {
                dates.add(new Date(startDateMillis + ((long)86400000 * i)));
            }
        } else {
            long startDateMillis = date.getTime() - ((date.getDate()-1) * (long)86400000);
            int i=0;
            Date tmpDate = new Date(startDateMillis + ((long)86400000 * i));
            while(tmpDate.getMonth() == date.getMonth()) {
                dates.add(tmpDate);
                ++i;
                tmpDate = new Date(startDateMillis + ((long)86400000 * i));
            }
        }
        this.setDatesSelected((Date[])dates.toArray(new Date[] {}));
    }
    
    /**
     * Sets the currently selected dates to the given collection.
     * 
     * @param dates The dates to set as selected in the Calendar
     */
    public void setDatesSelected(Date[] dates) {
        this.datesSelected = dates;
        // Update the date for panel building
        this.setPanelDate(this.getDatesSelected()[0]);
        // Refresh the panel
        this.refreshPanel();
        // Trigger the listeners update
        for (int i=0; i<this.updateListeners.size(); i++) {
            ((CalendarEventListener)this.updateListeners.get(i)).onDateUpdate(this);
        }
    }
    
    /**
     * Adds the given listener to the list of subscribers for date updates on this Calendar.
     * 
     * Any object passed should implement the CalendarEventListener interface, and will
     * receive an event in the form of a method invokation whenever this Calendar's selected
     * list of dates is updated.
     * 
     * @param listener
     */
    public void addCalendarUpdateListener(CalendarEventListener listener) {
        this.updateListeners.add(listener);
        listener.onDateUpdate(this);
    }
    
    /**
     * Returns the list of currently selected Dates.
     * 
     * @return The list of currently selected Dates
     */
    public Date[] getDatesSelected() {
        return this.datesSelected;
    }
    
    /**
     * Returns true if the given date is the same as one of the currently selected Dates, False otherwise.
     * 
     * The comparison takes only the year, month and day of the month into account, ignoring the time part
     * of the Date object.
     * 
     * @param date The Date object to be compared against the current list of selected Dates
     * 
     * @return True if the given Date matches one of the currently selected Dates, False otherwise
     */
    public boolean isSelectedDate(Date date) {
        for (int i=0; i<this.datesSelected.length; i++) {
            if (datesSelected[i].getYear() == date.getYear() && 
            		datesSelected[i].getMonth() == date.getMonth() &&
            		datesSelected[i].getDate() == date.getDate()) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * Returns the currently selected period size mode.
     * 
     * @return The currently selected period size mode
     * @see Calendar.selectionMode
     */
    public int getSelectionMode() {
        return this.selectionMode;
    }
    
    /**
     * Sets the currently selected period size mode.
     * 
     * @param selectionMode An integer matching one of the possible selection modes
     * @see Calendar.selectionMode
     */
    public void setSelectionMode(int selectionMode) {
    	if (selectionMode != Calendar.DAILY && selectionMode != Calendar.MONTHLY &&
    			selectionMode != Calendar.WEEKLY && selectionMode != Calendar.NEXT4DAYS) {
    		throw new IllegalArgumentException("Selection mode " + selectionMode + " does not match any known selection mode");
    	}
        this.selectionMode = selectionMode;
        this.setDatesFromDate(this.datesSelected[0]);
    }
    
    /**
     * Moves the group of selected dates to the previous group.
     * 
     * The actual resulting dates depend on the currently selected period size (NEXT4DAYS, WEEKLY, ...)
     * 
     * @see Calendar.selectionMode
     */
    public void moveToPrevious() {
        if (this.getSelectionMode() == Calendar.DAILY) {
            this.setDatesFromDate(new Date(this.datesSelected[0].getTime() - 86400000));
        } else if (this.getSelectionMode() == Calendar.NEXT4DAYS) {
            this.setDatesFromDate(new Date(this.datesSelected[0].getTime() - (86400000 * 4)));    
        } else {
            this.setDatesFromDate(new Date(this.datesSelected[0].getTime() - 86400000));
        }
    }
    
    /**
     * Moves the group of selected dates to the next group.
     * 
     * The actual resulting dates depend on the currently selected period size (NEXT4DAYS, WEEKLY, ...)
     * 
     * @see Calendar.selectionMode
     */
    public void moveToNext() {
        this.setDatesFromDate(new Date(this.datesSelected[this.datesSelected.length-1].getTime() + 86400000));        
    }
    
    /**
     * Refreshes the Calendar panel, using the panel date as a reference and taking into account
     * the currently selected dates.
     */
    public void refreshPanel() {
        this.calendarPanel.remove(this.monthDayListPanel);
        
        // The grid containing the days in the selected month
        this.monthDayListPanel = new VerticalPanel();
        this.monthDayListPanel.setWidth("100%");
        this.monthDayListPanel.setStyleName("kumulus-Calendar-mdlp");

        // Take a reference date from the list of selected dates
        Date referenceDate = this.datePanel;

        this.monthLink.setText(Calendar.fullMonthYearFormat.format(referenceDate));
        
        // Convert to milliseconds so that we can easily do date calculations using timestamps
        // as there is no support for the Calendar class in GWT
        long startDateMillis = referenceDate.getTime() - ((referenceDate.getDay() + 6) * 86400000);
        
        // Build the day list starting one week before the first day of the week of the reference date
        for (int i=0; i<=6; i++) {
            // One full week in the day list
            HorizontalPanel weekPanel = new HorizontalPanel();
            weekPanel.setWidth("100%");
            weekPanel.setHorizontalAlignment(HorizontalPanel.ALIGN_CENTER);
            for (int j=0; j<=6; j++) {
                // One day panel in the week list panel
                CalendarDay day = new CalendarDay(this, new Date(startDateMillis + ((long)86400000 * (i*7+j))));
                weekPanel.add(day);
                weekPanel.setCellWidth(day, "14%");
                // Set the layout according to the date in this cell
                if (this.isSelectedDate(day.getDate())) {
                    day.setSelected();
                }
                else if (day.getDate().getMonth() != referenceDate.getMonth()) {                	
                    day.setInactive();
                }
            }
            this.monthDayListPanel.add(weekPanel);
        }
        
        this.calendarPanel.add(this.monthDayListPanel);
    }
    
    /**
     * Handles all the events related to click actions on the different Calendar widgets.
     */
    public void onClick(Widget clickedWidget) {
        if (clickedWidget == this.prevMonthLink) {
            this.movePanelToPrevious();
        } else if (clickedWidget == this.nextMonthLink) {
            this.movePanelToNext();
        } else if (clickedWidget == this.monthLink) {
            
        }
    }
    
    /**
     * Class definition for a day panel in the Calendar widget.
     */
    private class CalendarDay extends Composite implements ClickListener {
        
    	/**
    	 * The Calendar widget hosting this CalendarDay.
    	 */
        private Calendar calendar;
        
        /**
         * The Date object corresponding to this CalendarDay widget.
         */
        private Date date;
        
        /**
         * The panel object hosting this widget.
         */
        private SimplePanel dayPanel = new SimplePanel();
        
        /**
         * The panel object hosting the text representation of this widget.
         */
        private SimplePanel dayTextPanel = new SimplePanel();
        
        /**
         * The link action corresponding to the onClick event on this widget.
         */
        private Hyperlink dayLink = new Hyperlink();
        
        /**
         * Class constructor.
         * 
         * @param calendar The Calendar object hosting this CalendarDay
         * @param date The Date object to assign to this CalendarDay
         */
        CalendarDay(Calendar calendar, Date date) {
            this.date = date;
            this.calendar = calendar;
            
            // The panel surrounding the day number
            this.dayLink.setText("" + this.date.getDate());
            this.dayTextPanel.add(this.dayLink);
            this.dayPanel.add(dayTextPanel);
            this.dayLink.addClickListener(this);
            
            // Special style for weekend days
            if (this.date.getDay() == 0 || this.date.getDay() == 6) {
                this.dayPanel.setStyleName("kumulus-Calendar-mdlp-wend");
            }
            
            initWidget(this.dayPanel);
        }
        
        /**
         * Returns the date corresponding to this CalendarDay.
         * 
         * @return The Date corresponding to this CalendarDay
         */
        public Date getDate() {
            return this.date;
        }
        
        /**
         * Sets this widget as inactive.
         */
        public void setInactive() {
            this.dayTextPanel.setStyleName("kumulus-Calendar-inactive");
        }
        
        /**
         * Sets this widget as selected.
         */
        public void setSelected() {
            this.dayPanel.setStyleName("kumulus-Calendar-mdlp-selected");
        }
        
        /**
         * Handler for all click events on this widget.
         * 
         * @param clickedWidget The widget object that was clicked
         */
        public void onClick(Widget clickedWidget) {
            this.calendar.setDatesFromDate(this.date);
        }
        
    }

}

